package com.mihuo.config.filter;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StreamUtils;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.StandardCharsets;

/**
 * 可缓存请求体的 HttpServletRequest 包装类
 * <p>
 * 解决 HTTP 请求输入流（InputStream）只能读取一次的问题。
 * 通过缓存请求体内容，支持后续处理器（如Controller中的@RequestBody）多次读取。
 * 通常用于过滤器（Filter）或拦截器（Interceptor）中包装原始请求。
 * </p>
 *
 * @author mihuo
 * @see HttpServletRequestWrapper
 */
@Slf4j
public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {

    /**
     * 缓存的请求体字节数组
     */
    private final byte[] cachedBody;

    /**
     * 日志输出最大字节数
     */
    private final static Integer logMaxSize = 1024 * 1024;

    /**
     * 构造方法，包装原始请求并缓存其请求体
     *
     * @param request 原始的 HttpServletRequest 对象
     * @throws IOException 当读取请求输入流发生错误时抛出
     */
    public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
        super(request);
        // 读取原始请求的输入流并转换为字节数组缓存起来
        this.cachedBody = StreamUtils.copyToByteArray(request.getInputStream());
    }

    /**
     * 重写 getInputStream 方法
     * <p>
     * 返回一个基于缓存字节数组的新 ServletInputStream，
     * 使得后续可以多次读取请求体内容。
     * </p>
     *
     * @return 基于缓存数据的新 ServletInputStream
     */
    @Override
    public ServletInputStream getInputStream() {
        // 使用缓存的字节数组创建新的输入流
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(cachedBody);

        return new ServletInputStream() {
            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }

            @Override
            public boolean isFinished() {
                // 当流中无可用数据时返回 true
                return byteArrayInputStream.available() == 0;
            }

            @Override
            public boolean isReady() {
                // 此实现始终返回 true，表示流已准备就绪可读取
                return true;
            }

            @Override
            public void setReadListener(ReadListener listener) {
                // 此实现未设置监听器，如需异步支持需在此实现
                throw new UnsupportedOperationException("setReadListener is not implemented");
            }
        };
    }

    /**
     * 重写 getReader 方法
     * <p>
     * 返回一个基于缓存字节数组的新 BufferedReader，
     * 使得后续可以多次读取请求体内容。
     * </p>
     *
     * @return 基于缓存数据的新 BufferedReader
     * @throws IOException 当创建 InputStreamReader 发生错误时抛出
     */
    @Override
    public BufferedReader getReader() throws IOException {
        // 返回一个基于缓存字节数组的新 BufferedReader
        return new BufferedReader(new InputStreamReader(this.getInputStream(), this.getCharacterEncoding()));
    }

    /**
     * 获取缓存的请求体内容（字符串形式）
     * <p>
     * 使用请求的字符编码进行解码。如果请求未指定字符编码，则使用系统默认编码（可能产生乱码）。
     * 更推荐使用 getCachedBody(Charset charset) 方法指定编码。
     * </p>
     *
     * @return 缓存的请求体字符串
     * @throws UnsupportedEncodingException 当请求的字符编码不支持时抛出
     */
    public String getCachedBody() throws UnsupportedEncodingException {
        String characterEncoding = this.getCharacterEncoding();
        if (characterEncoding == null) {
            characterEncoding = StandardCharsets.UTF_8.name(); // 提供默认编码
        }
        return new String(cachedBody, characterEncoding);
    }

    /**
     * 获取缓存的请求体内容，并尝试格式化为JSON字符串（如果内容是JSON）
     * <p>
     * 使用 Hutool 工具类进行 JSON 校验和格式化。
     * 如果请求体不是有效的 JSON 字符串，则返回原始字符串。
     * </p>
     *
     * @return 格式化的 JSON 字符串或原始请求体字符串；如果发生编码异常则返回 null
     */
    public String getCacheBodyJsonStr() {
        if(cachedBody == null) {return null;}
        if(cachedBody.length == 0) {return null;}
        if(cachedBody.length > logMaxSize) {return "Body Size > LogMaxSize";}
        String body = new String(cachedBody, StandardCharsets.UTF_8);
        if (StrUtil.isNotBlank(body) && JSONUtil.isJson(body)) {
            // 使用 Hutool 格式化 JSON，缩进显示
            return JSONUtil.parseObj(body).toString();
        }
        return body;
    }

    /**
     * 直接获取缓存的原始字节数组
     * <p>
     * 适用于需要直接处理字节数据的场景。
     * </p>
     *
     * @return 缓存的请求体字节数组
     */
    public byte[] getCachedBodyAsBytes() {
        return cachedBody.clone(); // 返回副本以避免外部修改破坏缓存数据
    }
}